RISC-V OSes in QEMU
This post is just about getting some OSes up in RISC-V. As much as I like running baremetal, it's nice to run some more abstracted software. The RISC-V - Getting Started Guide has some very nice guides for it.
Zephyr
Zephyr is a security-minded RTOS for embedded systems. I haven't ended up using it but it'll be nice to check out. For most of my use cases, I have mainly had a smaller, unsophisticated mcu, or just a computer running linux, but most of things I've worked on are quick turnaround one offs. Adding some sophistication and options can really help for longer and wider-scope projects. Most of my projects also had the worst security failure to be that the lights were the wrong color. Anyway, following the RISC-V docs, I installed dependencies.
sudo apt-get install --no-install-recommends git cmake ninja-build gperf \
ccache dfu-util device-tree-compiler wget python3-pip python3-setuptools \
python3-wheel xz-utils file make gcc gcc-multilib
After checking the zephyr getting started guide. I created a virtual environment to silo off the python requirements. Zephyr uses west
which they made for meta management.
Creating a virtual environment:
$ mkdir zephyr
$ cd zephyr
$ python3 -m venv .venv
Activate the environment:
$ source .venv/bin/activate
Once in the virtual environment, python packages will only be installed in that scope. First install west
$ pip install west
And then get the source code:
# in ur projects parent directory
$ west init zephyr
$ cd zephyr
$ west update
Then I exported the CMake package:
$ west zephyr-export
And installed python dependencies and sdk
$ west packages pip --install
And installed the sdk, I had to run around the permissions/virtual environment issue and this workaround was functional.
$ sudo -E PATH="$PATH" west sdk install --install-dir /opt/zephyr-sdk
That is everything installed! Now I can run an example:
$ mkdir build-example
$ cd build-example
$ cmake -DBOARD=qemu_riscv32 $ZEPHYR_BASE/samples/hello_world
$ make -j $(nproc)
Note that $ZEPHYR_BASE
is set to the location of the zephyr
folder which was made with west init
.
and run:
$ make run
And we get output!
[QEMU] CPU: riscv64
*** Booting Zephyr OS build v4.1.0-1109-g8b77098ca135 ***
Hello World! qemu_riscv64/qemu_virt_riscv64
I'll circle back to Zephyr again, it has some nice features I saw while looking around waiting for installs and downloads. The integrated test environment seems particularly interesting, having never really found a fast and good testing framework for embedded systems.
Linux
Yay linux! I also just wanted to get a basic linux OS up. This workflow requires qemu
, linux
, busybox
and the rust toolchain which I had already installed from source. Busybox is a nice set of UNIX utilities for embedded development.
First I downloaded the sources:
$ git clone https://github.com/torvalds/linux
$ git clone https://git.busybox.net/busybox
I already have QEMU setup, so then I built and compiled linux for a RISC-V target:
$ cd linux
$ make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- defconfig
$ make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- -j $(nproc)
And then built busybox
$ cd busybox
$ CROSS_COMPILE=riscv64-unknown-linux-gnu- make defconfig
$ CROSS_COMPILE=riscv64-unknown-linux-gnu- make -j $(nproc)
Then to run my QEMU machine:
$ sudo qemu-system-riscv64 -nographic -machine virt \
-kernel linux/arch/riscv/boot/Image -append "root=/dev/vda ro console=ttyS0" \
-drive file=busybox/busybox,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0
So this is from the RISC-V docs, and something has changed, I get a kernel panic trying to mound /dev/vda
.
The reason looks like I don't have a filesystem setup. Going from this source, I create a filesystem structure:
mkdir initramfs
cd initramfs
mkdir -p {bin,sbin,dev,etc,home,mnt,proc,sys,usr,tmp}
mkdir -p usr/{bin,sbin}
mkdir -p proc/sys/kernel
cd dev
sudo mknod sda b 8 0
sudo mknod console c 5 1
cd ..
I copy the busybox
executable into bin and then create the filesystem:
$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > initramfs.cpio.gz
And I try to run
$ qemu-system-riscv64 -nographic -machine virt \
-kernel linux/arch/riscv/boot/Image \
-initrd initramfs/initramfs.cpio.gz \
-append "console=ttyS0"
Which still gives me a similarly panic, so I'm missing something. After looking through a few forums which really didn't give a clear answer, I got to this post Linux & Python on RISC-V using QEMU from scratch. Instead of making the init filesystem a regular file, they created a null disk and created the filesystem in there.
Creating the NULL disk and formatting it:
$ dd if=/dev/zero of=root.bin bs=1M count=64
$ mkfs.ext2 -F root.bin
And setting setting up the fs, and setting busybox as the init:
mkdir mnt
sudo mount -o loop root.bin mnt
cd mnt
sudo mkdir -p bin etc dev lib proc sbin tmp usr usr/bin usr/lib usr/sbin
sudo cp ~/busybox/busybox bin
sudo ln -s ../bin/busybox sbin/init
sudo ln -s ../bin/busybox bin/sh
cd ..
sudo umount mnt
Then I was able to launch QEMU with this command:
$ qemu-system-riscv64 -nographic -machine virt \
-kernel linux/arch/riscv/boot/Image \
-append "root=/dev/vda rw console=ttyS0" \
-drive file=root.bin,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0
I'm not sure if the difference was my QEMU configuration, or if this was implicityl set up somewhere else. Bouncing around forums, it seems there are some toolkits for getting linux up and running, but this does launch. When QEMU boots, I get a console terminal. Install busybox tools using:
# /bin/busybox --install -s
Now I have access to basic unix utilities in a RISCV environment.
# uname -a
Linux (none) 6.12.0 #2 SMP Sat Mar 22 11:24:52 EDT 2025 riscv64 GNU/Linux
That's good progress for today. It's nice to have all these little containers running architectures.